package tr.edu.metu.is.mega.server.core;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.eclipse.persistence.expressions.Expression;

import tr.edu.metu.is.mega.server.dao.EntityManagerUtil;

/**
 * Abstract DAO implementation to be base class of all data access objects.
 * 
 * @param <T> type modifier
 * @param <IdT> type modifier of primary key
 */
public abstract class AbstractDAO<T extends BaseEntity, IdT> implements DAO<T, IdT> {

    private final Class<T> entityClass;

    private final EntityManager entityManager;

    private final EntitySearchHelper<T> entitySearchHelper;

    /**
     * Default constructor.
     */
    @SuppressWarnings("all")
    public AbstractDAO() {

        entityManager = EntityManagerUtil.getInstance().createEntityManager();
        entityManager.setFlushMode(FlushModeType.AUTO);
        entitySearchHelper = new EntitySearchHelper<T>();
        ParameterizedType genericSuperClass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperClass.getActualTypeArguments()[0];
        entityClass = (Class<T>) type;
    }

    /**
     * Returns entity manager.
     * 
     * @return the entity manager
     */
    public EntityManager getEntityManager() {
        return this.entityManager;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T create(T entity) {
        entityManager.getTransaction().begin();
        entityManager.persist(entity);
        entityManager.getTransaction().commit();
        return entity;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T update(T entity) {
        entityManager.getTransaction().begin();
        entityManager.merge(entity);
        entityManager.getTransaction().commit();
        return entity;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public T findById(IdT id) {
        return entityManager.find(entityClass, id);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void delete(T entity) {
        entityManager.getTransaction().begin();
        entityManager.remove(entityManager.merge(entity));
        entityManager.getTransaction().commit();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteById(IdT id) {
        entityManager.getTransaction().begin();
        entityManager.remove(findById(id));
        entityManager.getTransaction().commit();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> loadAll() {
        return loadAllHelper(null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> loadAll(PagedQueryParams params) {
        return loadAllHelper(params);
    }

    /**
     * Helper method for loadAll method.
     * 
     * @param params query parameters
     * @return query result
     */
    private List<T> loadAllHelper(PagedQueryParams params) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> cq = cb.createQuery(entityClass);
        Root<T> root = cq.from(entityClass);
        cq.select(root);

        TypedQuery<T> q = null;

        if (params == null) {
            q = entityManager.createQuery(cq);
        } else {
            if (params.getDirection() != null) {

                Path<?> sortPath = getSortPath(root, params.getSortField());

                if (params.getDirection() == SortDirection.ASC) {
                    cq.orderBy(cb.asc(sortPath));
                } else {
                    cq.orderBy(cb.desc(sortPath));
                }
            }

            q = entityManager.createQuery(cq);
            q.setMaxResults(params.getMaxResults());
            q.setFirstResult(params.getFirstResult());

        }

        return q.getResultList();
    }

    /**
     * SortField olarak detay entity'ye ait fieldler: fkBirthplaceid.description
     * seklinde gonderilirse detay entity'ye ait belirtilen field'a gore order
     * by yapmak icin eklendi.
     * 
     * @param root root
     * @param sortField sort field
     * @return sort path
     */
    private Path<?> getSortPath(Root<T> root, String sortField) {
        String[] tokens = sortField.split("[.]");
        Path<?> sortPath = root.get(tokens[0]);
        for (int i = 1; i < tokens.length; i++) {
            sortPath = sortPath.get(tokens[i]);
        }
        return sortPath;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByExample(T example, boolean useLikeQueryForStringFields) {
        return entitySearchHelper.findByExample(example, entityManager, useLikeQueryForStringFields);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByExample(T example, boolean useLikeQueryForStringFields, PagedQueryParams params) {
        return entitySearchHelper.findByExample(example, entityManager, useLikeQueryForStringFields, params);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Long getCountByExample(T example, boolean useLikeQueryForStringFields) {
        return entitySearchHelper.findByExampleCount(example, entityManager, useLikeQueryForStringFields);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByParams(Map<String, Object> params) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> cq = cb.createQuery(entityClass);
        Root<T> root = cq.from(entityClass);
        Predicate p = cb.conjunction();
        for (Map.Entry<String, Object> param : params.entrySet()) {
            p = cb.and(p, cb.equal(root.get(param.getKey()), param.getValue()));
        }
        cq.select(root).where(p);
        TypedQuery<T> q = entityManager.createQuery(cq);
        return q.getResultList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByMultipleValue(String fieldName, Object... prms) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> cq = cb.createQuery(entityClass);
        Root<T> root = cq.from(entityClass);
        cq.select(root).where(root.get(fieldName).in(prms));
        TypedQuery<T> q = entityManager.createQuery(cq);
        return q.getResultList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByIdList(IdT... ids) {
        return findByMultipleValue("pkid", ids);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Long getRecordCount() {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Long> cq = cb.createQuery(Long.class);
        cq.select(cb.count(cq.from(entityClass)));
        return entityManager.createQuery(cq).getSingleResult();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteByIdList(List<IdT> idList) {
        entityManager.createQuery("DELETE FROM " + entityClass.getSimpleName() + " e WHERE e.pkid in :idlist ").setParameter("idlist", idList)
                .executeUpdate();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByExample(T example, boolean useLikeQueryForStringFields, PagedQueryParams params, Expression expr) {
        return entitySearchHelper.findByExample(example, entityManager, useLikeQueryForStringFields, params, expr);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Long getCountByExample(T example, boolean useLikeQueryForStringFields, Expression expr) {
        return entitySearchHelper.findByExampleCount(example, entityManager, useLikeQueryForStringFields, expr);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByNamedNativeQuery(String queryName, Map<Integer, Object> queryParams) {
        return JpaQueryUtil.findByNamedNativeQuery(entityManager, queryName, queryParams, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <X> X findSingleResultByNamedNativeQuery(String queryName, Map<Integer, Object> queryParams) {
        return JpaQueryUtil.findSingleResultByNamedNativeQuery(entityManager, queryName, queryParams);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByNamedQuery(String queryName, Map<String, Object> params) {
        return JpaQueryUtil.findByNamedQuery(entityManager, queryName, params, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public <X> X findSingleResultByNamedQuery(String queryName, Map<String, Object> params) {
        return JpaQueryUtil.findSingleResultByNamedQuery(entityManager, queryName, params);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<T> findByQuery(String queryStr, Map<String, Object> queryParams, PagedQueryParams params) {
        TypedQuery<T> query = entityManager.createQuery(queryStr, entityClass);
        for (Map.Entry<String, Object> queryParam : queryParams.entrySet()) {
            query = query.setParameter(queryParam.getKey(), queryParam.getValue());
        }
        query.setMaxResults(params.getMaxResults());
        query.setFirstResult(params.getFirstResult());
        return query.getResultList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Long findByQueryCount(String queryStr, Map<String, Object> queryParams) {
        Query query = entityManager.createQuery(queryStr);
        for (Map.Entry<String, Object> queryParam : queryParams.entrySet()) {
            query = query.setParameter(queryParam.getKey(), queryParam.getValue());
        }

        return (Long) query.getSingleResult();
    }

}
